<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <title>基于美学的动态交互</title>
  <script src="https://d3js.org/d3.v7.min.js"></script>
  <style>
    body {
      font-family: sans-serif;
    }

    #metrics {
      position: absolute;
      top: 10px;
      right: 10px;
      background: #f0f0f0;
      padding: 10px;
      border-radius: 8px;
      box-shadow: 0 0 8px rgba(0, 0, 0, 0.2);
    }

    svg {
      border: 1px solid #ccc;
    }

    .node {
      cursor: grab;
    }
  </style>
</head>

<body>
  <div id="metrics">
    <strong>美学指标</strong><br>
    节点分布: <span id="uniformity">0</span><br>
    边长度的标准差: <span id="stddev">0</span><br>
    边交叉率: <span id="crossings">0</span><br>
    对称性: <span id="symmetry">0</span><br>
    群体紧密性: <span id="tightness">0</span><br>
    角度分布均匀性: <span id="angular">0</span><br>

  </div>
  <svg id="graph" width="1700" height="900"></svg>

  <script>
    const width = 900, height = 960;




    const svg = d3.select("#graph");

    // ⬇️ 添加 g 容器以支持缩放和平移
    const container = svg.append("g");
    const nodes = [
      { id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }, { id: 5 }, { id: 6 }, { id: 7 }, { id: 8 }, { id: 9 },
      { id: 10 }, { id: 11 }, { id: 12 }, { id: 13 }, { id: 14 }, { id: 15 }, { id: 16 }, { id: 17 }, { id: 18 }, { id: 19 },
      { id: 20 }, { id: 21 }, { id: 22 }, { id: 23 }, { id: 24 }, { id: 25 }, { id: 26 }, { id: 27 }, { id: 28 }, { id: 29 },
      { id: 30 }, { id: 31 }, { id: 32 }, { id: 33 }, { id: 34 }, { id: 35 }, { id: 36 }, { id: 37 }, { id: 38 }, { id: 39 },
      { id: 40 }, { id: 41 }, { id: 42 }, { id: 43 }, { id: 44 }, { id: 45 }, { id: 46 }, { id: 47 }, { id: 48 }, { id: 49 },
      { id: 50 }, { id: 51 }, { id: 52 }, { id: 53 }, { id: 54 }, { id: 55 }, { id: 56 }, { id: 57 }, { id: 58 }, { id: 59 },
      { id: 60 }, { id: 61 }, { id: 62 }, { id: 63 }, { id: 64 }, { id: 65 }, { id: 66 }, { id: 67 }, { id: 68 }, { id: 69 },
      { id: 70 }, { id: 71 }, { id: 72 }, { id: 73 }, { id: 74 }, { id: 75 }, { id: 76 }, { id: 77 }, { id: 78 }, { id: 79 },
      { id: 80 }, { id: 81 }, { id: 82 }, { id: 83 }, { id: 84 }, { id: 85 }, { id: 86 }, { id: 87 }, { id: 88 }, { id: 89 },
      { id: 90 }, { id: 91 }, { id: 92 }, { id: 93 }, { id: 94 }, { id: 95 }, { id: 96 }, { id: 97 }, { id: 98 }, { id: 99 },
      { id: 100 }
    ];

    const links = [{ 'source': 1, 'target': 23, 'weight': 1 }, { 'source': 1, 'target': 25, 'weight': 1 }, { 'source': 1, 'target': 2, 'weight': 1 },
    { 'source': 1, 'target': 3, 'weight': 1 }, { 'source': 1, 'target': 13, 'weight': 1 }, { 'source': 23, 'target': 4, 'weight': 1 }, { 'source': 23, 'target': 22, 'weight': 1 },
    { 'source': 23, 'target': 5, 'weight': 1 }, { 'source': 23, 'target': 17, 'weight': 1 }, { 'source': 23, 'target': 6, 'weight': 1 },
    { 'source': 23, 'target': 16, 'weight': 1 }, { 'source': 23, 'target': 58, 'weight': 1 }, { 'source': 23, 'target': 24, 'weight': 1 },
    { 'source': 25, 'target': 3, 'weight': 1 }, { 'source': 25, 'target': 10, 'weight': 1 }, { 'source': 25, 'target': 17, 'weight': 1 },
    { 'source': 25, 'target': 22, 'weight': 1 }, { 'source': 25, 'target': 6, 'weight': 1 }, { 'source': 25, 'target': 21, 'weight': 1 },
    { 'source': 25, 'target': 27, 'weight': 1 }, { 'source': 2, 'target': 21, 'weight': 1 }, { 'source': 2, 'target': 11, 'weight': 1 },
    { 'source': 2, 'target': 19, 'weight': 1 }, { 'source': 2, 'target': 17, 'weight': 1 }, { 'source': 21, 'target': 18, 'weight': 1 },
    { 'source': 21, 'target': 19, 'weight': 1 }, { 'source': 21, 'target': 14, 'weight': 1 }, { 'source': 21, 'target': 21, 'weight': 1 },
    { 'source': 21, 'target': 40, 'weight': 1 }, { 'source': 11, 'target': 24, 'weight': 1 }, { 'source': 11, 'target': 9, 'weight': 1 },
    { 'source': 11, 'target': 14, 'weight': 1 }, { 'source': 19, 'target': 12, 'weight': 1 }, { 'source': 19, 'target': 7, 'weight': 1 },
    { 'source': 19, 'target': 24, 'weight': 1 }, { 'source': 19, 'target': 16, 'weight': 1 }, { 'source': 17, 'target': 5, 'weight': 1 },
    { 'source': 17, 'target': 6, 'weight': 1 }, { 'source': 17, 'target': 14, 'weight': 1 }, { 'source': 17, 'target': 15, 'weight': 1 },
    { 'source': 17, 'target': 17, 'weight': 1 }, { 'source': 17, 'target': 33, 'weight': 1 }, { 'source': 17, 'target': 20, 'weight': 1 },
    { 'source': 3, 'target': 7, 'weight': 1 }, { 'source': 3, 'target': 13, 'weight': 1 }, { 'source': 3, 'target': 4, 'weight': 1 },
    { 'source': 3, 'target': 9, 'weight': 1 }, { 'source': 3, 'target': 18, 'weight': 1 }, { 'source': 3, 'target': 83, 'weight': 1 },
    { 'source': 7, 'target': 4, 'weight': 1 }, { 'source': 7, 'target': 5, 'weight': 1 }, { 'source': 7, 'target': 9, 'weight': 1 },
    { 'source': 7, 'target': 92, 'weight': 1 }, { 'source': 7, 'target': 14, 'weight': 1 }, { 'source': 7, 'target': 76, 'weight': 1 },
    { 'source': 13, 'target': 20, 'weight': 1 }, { 'source': 13, 'target': 15, 'weight': 1 }, { 'source': 4, 'target': 12, 'weight': 1 },
    { 'source': 4, 'target': 20, 'weight': 1 }, { 'source': 4, 'target': 8, 'weight': 1 }, { 'source': 4, 'target': 16, 'weight': 1 },
    { 'source': 12, 'target': 12, 'weight': 1 }, { 'source': 12, 'target': 22, 'weight': 1 }, { 'source': 20, 'target': 8, 'weight': 1 },
    { 'source': 20, 'target': 14, 'weight': 1 }, { 'source': 20, 'target': 24, 'weight': 1 }, { 'source': 8, 'target': 5, 'weight': 1 },
    { 'source': 8, 'target': 15, 'weight': 1 }, { 'source': 8, 'target': 14, 'weight': 1 }, { 'source': 8, 'target': 22, 'weight': 1 },
    { 'source': 5, 'target': 9, 'weight': 1 }, { 'source': 5, 'target': 16, 'weight': 1 }, { 'source': 5, 'target': 22, 'weight': 1 },
    { 'source': 5, 'target': 35, 'weight': 1 }, { 'source': 9, 'target': 24, 'weight': 1 }, { 'source': 9, 'target': 10, 'weight': 1 },
    { 'source': 6, 'target': 10, 'weight': 1 }, { 'source': 6, 'target': 24, 'weight': 1 }, { 'source': 6, 'target': 18, 'weight': 1 },
    { 'source': 6, 'target': 15, 'weight': 1 }, { 'source': 6, 'target': 45, 'weight': 1 }, { 'source': 10, 'target': 15, 'weight': 1 },
    { 'source': 10, 'target': 22, 'weight': 1 }, { 'source': 10, 'target': 18, 'weight': 1 }, { 'source': 10, 'target': 24, 'weight': 1 },
    { 'source': 24, 'target': 14, 'weight': 1 }, { 'source': 24, 'target': 15, 'weight': 1 }, { 'source': 18, 'target': 15, 'weight': 1 },
    { 'source': 18, 'target': 22, 'weight': 1 }, { 'source': 18, 'target': 29, 'weight': 1 }, { 'source': 92, 'target': 79, 'weight': 1 },
    { 'source': 92, 'target': 82, 'weight': 1 }, { 'source': 92, 'target': 84, 'weight': 1 }, { 'source': 92, 'target': 88, 'weight': 1 },
    { 'source': 92, 'target': 91, 'weight': 1 }, { 'source': 92, 'target': 96, 'weight': 1 }, { 'source': 92, 'target': 94, 'weight': 1 },
    { 'source': 15, 'target': 14, 'weight': 1 }, { 'source': 15, 'target': 22, 'weight': 1 }, { 'source': 15, 'target': 15, 'weight': 1 },
    { 'source': 14, 'target': 39, 'weight': 1 }, { 'source': 14, 'target': 96, 'weight': 1 }, { 'source': 16, 'target': 16, 'weight': 1 },
    { 'source': 16, 'target': 47, 'weight': 1 }, { 'source': 33, 'target': 26, 'weight': 1 }, { 'source': 33, 'target': 27, 'weight': 1 },
    { 'source': 33, 'target': 36, 'weight': 1 }, { 'source': 33, 'target': 43, 'weight': 1 }, { 'source': 33, 'target': 28, 'weight': 1 },
    { 'source': 33, 'target': 57, 'weight': 1 }, { 'source': 58, 'target': 56, 'weight': 1 }, { 'source': 58, 'target': 57, 'weight': 1 },
    { 'source': 58, 'target': 72, 'weight': 1 }, { 'source': 58, 'target': 63, 'weight': 1 }, { 'source': 58, 'target': 69, 'weight': 1 },
    { 'source': 58, 'target': 75, 'weight': 1 }, { 'source': 58, 'target': 59, 'weight': 1 }, { 'source': 58, 'target': 65, 'weight': 1 },
    { 'source': 58, 'target': 74, 'weight': 1 }, { 'source': 26, 'target': 49, 'weight': 1 }, { 'source': 26, 'target': 40, 'weight': 1 },
    { 'source': 26, 'target': 27, 'weight': 1 }, { 'source': 26, 'target': 37, 'weight': 1 }, { 'source': 26, 'target': 38, 'weight': 1 },
    { 'source': 26, 'target': 44, 'weight': 1 }, { 'source': 26, 'target': 48, 'weight': 1 }, { 'source': 49, 'target': 29, 'weight': 1 },
    { 'source': 49, 'target': 31, 'weight': 1 }, { 'source': 49, 'target': 28, 'weight': 1 }, { 'source': 49, 'target': 69, 'weight': 1 },
    { 'source': 40, 'target': 50, 'weight': 1 }, { 'source': 40, 'target': 44, 'weight': 1 }, { 'source': 40, 'target': 42, 'weight': 1 },
    { 'source': 40, 'target': 43, 'weight': 1 }, { 'source': 27, 'target': 43, 'weight': 1 }, { 'source': 27, 'target': 45, 'weight': 1 },
    { 'source': 27, 'target': 38, 'weight': 1 }, { 'source': 27, 'target': 36, 'weight': 1 }, { 'source': 27, 'target': 44, 'weight': 1 },
    { 'source': 27, 'target': 47, 'weight': 1 }, { 'source': 43, 'target': 41, 'weight': 1 }, { 'source': 43, 'target': 48, 'weight': 1 },
    { 'source': 43, 'target': 45, 'weight': 1 }, { 'source': 43, 'target': 50, 'weight': 1 }, { 'source': 43, 'target': 32, 'weight': 1 },
    { 'source': 43, 'target': 47, 'weight': 1 }, { 'source': 45, 'target': 35, 'weight': 1 }, { 'source': 45, 'target': 34, 'weight': 1 },
    { 'source': 45, 'target': 41, 'weight': 1 }, { 'source': 45, 'target': 30, 'weight': 1 }, { 'source': 45, 'target': 29, 'weight': 1 },
    { 'source': 45, 'target': 38, 'weight': 1 }, { 'source': 38, 'target': 31, 'weight': 1 }, { 'source': 38, 'target': 34, 'weight': 1 },
    { 'source': 38, 'target': 50, 'weight': 1 }, { 'source': 38, 'target': 61, 'weight': 1 }, { 'source': 28, 'target': 39, 'weight': 1 },
    { 'source': 28, 'target': 32, 'weight': 1 }, { 'source': 28, 'target': 29, 'weight': 1 }, { 'source': 28, 'target': 30, 'weight': 1 },
    { 'source': 28, 'target': 34, 'weight': 1 }, { 'source': 39, 'target': 29, 'weight': 1 }, { 'source': 39, 'target': 36, 'weight': 1 },
    { 'source': 39, 'target': 46, 'weight': 1 }, { 'source': 39, 'target': 41, 'weight': 1 }, { 'source': 39, 'target': 99, 'weight': 1 },
    { 'source': 32, 'target': 36, 'weight': 1 }, { 'source': 32, 'target': 29, 'weight': 1 }, { 'source': 32, 'target': 32, 'weight': 1 },
    { 'source': 32, 'target': 87, 'weight': 1 }, { 'source': 32, 'target': 50, 'weight': 1 }, { 'source': 29, 'target': 34, 'weight': 1 },
    { 'source': 29, 'target': 48, 'weight': 1 }, { 'source': 29, 'target': 31, 'weight': 1 }, { 'source': 29, 'target': 36, 'weight': 1 },
    { 'source': 29, 'target': 37, 'weight': 1 }, { 'source': 29, 'target': 47, 'weight': 1 }, { 'source': 29, 'target': 65, 'weight': 1 },
    { 'source': 34, 'target': 50, 'weight': 1 }, { 'source': 34, 'target': 78, 'weight': 1 }, { 'source': 34, 'target': 35, 'weight': 1 },
    { 'source': 34, 'target': 44, 'weight': 1 }, { 'source': 34, 'target': 61, 'weight': 1 }, { 'source': 48, 'target': 42, 'weight': 1 },
    { 'source': 48, 'target': 36, 'weight': 1 }, { 'source': 48, 'target': 31, 'weight': 1 }, { 'source': 30, 'target': 46, 'weight': 1 },
    { 'source': 30, 'target': 30, 'weight': 1 }, { 'source': 30, 'target': 41, 'weight': 1 }, { 'source': 30, 'target': 96, 'weight': 1 },
    { 'source': 46, 'target': 37, 'weight': 1 }, { 'source': 46, 'target': 47, 'weight': 1 }, { 'source': 96, 'target': 80, 'weight': 1 },
    { 'source': 96, 'target': 88, 'weight': 1 }, { 'source': 96, 'target': 89, 'weight': 1 }, { 'source': 96, 'target': 97, 'weight': 1 },
    { 'source': 96, 'target': 84, 'weight': 1 }, { 'source': 31, 'target': 94, 'weight': 1 }, { 'source': 31, 'target': 42, 'weight': 1 },
    { 'source': 94, 'target': 85, 'weight': 1 }, { 'source': 94, 'target': 77, 'weight': 1 }, { 'source': 94, 'target': 81, 'weight': 1 },
    { 'source': 36, 'target': 44, 'weight': 1 }, { 'source': 36, 'target': 72, 'weight': 1 }, { 'source': 87, 'target': 82, 'weight': 1 },
    { 'source': 87, 'target': 83, 'weight': 1 }, { 'source': 87, 'target': 84, 'weight': 1 }, { 'source': 87, 'target': 81, 'weight': 1 },
    { 'source': 87, 'target': 79, 'weight': 1 }, { 'source': 87, 'target': 95, 'weight': 1 }, { 'source': 87, 'target': 70, 'weight': 1 },
    { 'source': 87, 'target': 98, 'weight': 1 }, { 'source': 57, 'target': 72, 'weight': 1 }, { 'source': 57, 'target': 74, 'weight': 1 },
    { 'source': 57, 'target': 59, 'weight': 1 }, { 'source': 57, 'target': 56, 'weight': 1 }, { 'source': 57, 'target': 63, 'weight': 1 },
    { 'source': 57, 'target': 70, 'weight': 1 }, { 'source': 57, 'target': 75, 'weight': 1 }, { 'source': 50, 'target': 42, 'weight': 1 },
    { 'source': 50, 'target': 82, 'weight': 1 }, { 'source': 78, 'target': 77, 'weight': 1 }, { 'source': 78, 'target': 97, 'weight': 1 },
    { 'source': 78, 'target': 85, 'weight': 1 }, { 'source': 78, 'target': 88, 'weight': 1 }, { 'source': 78, 'target': 89, 'weight': 1 },
    { 'source': 78, 'target': 86, 'weight': 1 }, { 'source': 78, 'target': 93, 'weight': 1 }, { 'source': 44, 'target': 44, 'weight': 1 },
    { 'source': 37, 'target': 47, 'weight': 1 }, { 'source': 37, 'target': 66, 'weight': 1 }, { 'source': 47, 'target': 42, 'weight': 1 },
    { 'source': 61, 'target': 52, 'weight': 1 }, { 'source': 61, 'target': 66, 'weight': 1 }, { 'source': 61, 'target': 59, 'weight': 1 },
    { 'source': 61, 'target': 61, 'weight': 1 }, { 'source': 61, 'target': 70, 'weight': 1 }, { 'source': 61, 'target': 62, 'weight': 1 },
    { 'source': 61, 'target': 63, 'weight': 1 }, { 'source': 61, 'target': 69, 'weight': 1 }, { 'source': 69, 'target': 59, 'weight': 1 },
    { 'source': 69, 'target': 71, 'weight': 1 }, { 'source': 69, 'target': 69, 'weight': 1 }, { 'source': 69, 'target': 63, 'weight': 1 },
    { 'source': 69, 'target': 72, 'weight': 1 }, { 'source': 82, 'target': 82, 'weight': 1 }, { 'source': 82, 'target': 97, 'weight': 1 },
    { 'source': 51, 'target': 52, 'weight': 1 }, { 'source': 51, 'target': 71, 'weight': 1 }, { 'source': 51, 'target': 68, 'weight': 1 },
    { 'source': 51, 'target': 60, 'weight': 1 }, { 'source': 51, 'target': 74, 'weight': 1 }, { 'source': 51, 'target': 75, 'weight': 1 },
    { 'source': 52, 'target': 70, 'weight': 1 }, { 'source': 52, 'target': 53, 'weight': 1 }, { 'source': 52, 'target': 63, 'weight': 1 },
    { 'source': 52, 'target': 55, 'weight': 1 }, { 'source': 52, 'target': 66, 'weight': 1 }, { 'source': 52, 'target': 68, 'weight': 1 },
    { 'source': 71, 'target': 62, 'weight': 1 }, { 'source': 71, 'target': 54, 'weight': 1 }, { 'source': 71, 'target': 63, 'weight': 1 },
    { 'source': 68, 'target': 59, 'weight': 1 }, { 'source': 68, 'target': 55, 'weight': 1 }, { 'source': 68, 'target': 68, 'weight': 1 },
    { 'source': 68, 'target': 53, 'weight': 1 }, { 'source': 68, 'target': 72, 'weight': 1 }, { 'source': 68, 'target': 70, 'weight': 1 },
    { 'source': 68, 'target': 74, 'weight': 1 }, { 'source': 70, 'target': 75, 'weight': 1 }, { 'source': 70, 'target': 53, 'weight': 1 },
    { 'source': 70, 'target': 89, 'weight': 1 }, { 'source': 53, 'target': 75, 'weight': 1 }, { 'source': 53, 'target': 55, 'weight': 1 },
    { 'source': 53, 'target': 63, 'weight': 1 }, { 'source': 53, 'target': 65, 'weight': 1 }, { 'source': 63, 'target': 72, 'weight': 1 },
    { 'source': 63, 'target': 60, 'weight': 1 }, { 'source': 63, 'target': 91, 'weight': 1 }, { 'source': 55, 'target': 56, 'weight': 1 },
    { 'source': 55, 'target': 72, 'weight': 1 }, { 'source': 55, 'target': 67, 'weight': 1 }, { 'source': 55, 'target': 65, 'weight': 1 },
    { 'source': 55, 'target': 64, 'weight': 1 }, { 'source': 55, 'target': 73, 'weight': 1 }, { 'source': 55, 'target': 74, 'weight': 1 },
    { 'source': 75, 'target': 67, 'weight': 1 }, { 'source': 54, 'target': 60, 'weight': 1 }, { 'source': 54, 'target': 73, 'weight': 1 },
    { 'source': 54, 'target': 56, 'weight': 1 }, { 'source': 60, 'target': 65, 'weight': 1 }, { 'source': 60, 'target': 62, 'weight': 1 },
    { 'source': 73, 'target': 72, 'weight': 1 }, { 'source': 73, 'target': 65, 'weight': 1 }, { 'source': 73, 'target': 74, 'weight': 1 },
    { 'source': 56, 'target': 65, 'weight': 1 }, { 'source': 72, 'target': 59, 'weight': 1 }, { 'source': 72, 'target': 66, 'weight': 1 },
    { 'source': 72, 'target': 72, 'weight': 1 }, { 'source': 67, 'target': 62, 'weight': 1 }, { 'source': 67, 'target': 64, 'weight': 1 },
    { 'source': 67, 'target': 84, 'weight': 1 }, { 'source': 65, 'target': 62, 'weight': 1 }, { 'source': 65, 'target': 74, 'weight': 1 },
    { 'source': 74, 'target': 66, 'weight': 1 }, { 'source': 59, 'target': 83, 'weight': 1 }, { 'source': 59, 'target': 64, 'weight': 1 },
    { 'source': 83, 'target': 100, 'weight': 1 }, { 'source': 83, 'target': 84, 'weight': 1 }, { 'source': 83, 'target': 95, 'weight': 1 },
    { 'source': 83, 'target': 86, 'weight': 1 }, { 'source': 83, 'target': 85, 'weight': 1 }, { 'source': 83, 'target': 91, 'weight': 1 },
    { 'source': 83, 'target': 93, 'weight': 1 }, { 'source': 83, 'target': 99, 'weight': 1 }, { 'source': 84, 'target': 76, 'weight': 1 },
    { 'source': 84, 'target': 91, 'weight': 1 }, { 'source': 84, 'target': 86, 'weight': 1 }, { 'source': 89, 'target': 81, 'weight': 1 },
    { 'source': 89, 'target': 86, 'weight': 1 }, { 'source': 89, 'target': 88, 'weight': 1 }, { 'source': 89, 'target': 91, 'weight': 1 },
    { 'source': 89, 'target': 100, 'weight': 1 }, { 'source': 76, 'target': 93, 'weight': 1 }, { 'source': 76, 'target': 95, 'weight': 1 },
    { 'source': 76, 'target': 98, 'weight': 1 }, { 'source': 76, 'target': 81, 'weight': 1 }, { 'source': 93, 'target': 90, 'weight': 1 },
    { 'source': 93, 'target': 93, 'weight': 1 }, { 'source': 93, 'target': 77, 'weight': 1 }, { 'source': 93, 'target': 98, 'weight': 1 },
    { 'source': 95, 'target': 88, 'weight': 1 }, { 'source': 95, 'target': 85, 'weight': 1 }, { 'source': 95, 'target': 77, 'weight': 1 },
    { 'source': 95, 'target': 98, 'weight': 1 }, { 'source': 98, 'target': 80, 'weight': 1 }, { 'source': 98, 'target': 86, 'weight': 1 },
    { 'source': 98, 'target': 98, 'weight': 1 }, { 'source': 77, 'target': 99, 'weight': 1 }, { 'source': 77, 'target': 91, 'weight': 1 },
    { 'source': 77, 'target': 79, 'weight': 1 }, { 'source': 99, 'target': 79, 'weight': 1 }, { 'source': 99, 'target': 90, 'weight': 1 },
    { 'source': 99, 'target': 81, 'weight': 1 }, { 'source': 91, 'target': 88, 'weight': 1 }, { 'source': 97, 'target': 81, 'weight': 1 },
    { 'source': 97, 'target': 100, 'weight': 1 }, { 'source': 97, 'target': 90, 'weight': 1 }, { 'source': 88, 'target': 79, 'weight': 1 },
    { 'source': 88, 'target': 86, 'weight': 1 }, { 'source': 88, 'target': 88, 'weight': 1 }, { 'source': 79, 'target': 81, 'weight': 1 },
    { 'source': 79, 'target': 100, 'weight': 1 }, { 'source': 80, 'target': 90, 'weight': 1 }, { 'source': 100, 'target': 90, 'weight': 1 },
    { 'source': 86, 'target': 90, 'weight': 1 }];



    const simulation = d3.forceSimulation(nodes)
      .force("link", d3.forceLink(links).id(d => d.id).distance(100))// 边连接力
      .force("charge", d3.forceManyBody().strength(-300))// 节点排斥力
      .force("center", d3.forceCenter(width / 2, height / 2))// 中心引力
      .on("tick", ticked);// 每帧更新回调
    // 边
    const link = container.selectAll(".link")
      .data(links)
      .join("line")
      .attr("stroke", "#999");
    //点
    const node = container.selectAll(".node")
      .data(nodes)
      .join("circle")
      .attr("r", 10)
      .attr("fill", "#69b3a2")
      .call(d3.drag()
        .on("start", dragstarted)
        .on("drag", dragged)
        .on("end", dragended));
    svg.call(d3.zoom()
      .scaleExtent([0.1, 5]) // 缩放范围
      .on("zoom", (event) => {
        container.attr("transform", event.transform);
      }));
    function ticked() {
      link
        .attr("x1", d => d.source.x)
        .attr("y1", d => d.source.y)
        .attr("x2", d => d.target.x)
        .attr("y2", d => d.target.y);

      node
        .attr("cx", d => d.x)
        .attr("cy", d => d.y);

      updateMetrics();
    }

    function dragstarted(event, d) {
      simulation.alphaTarget(0.3).restart();
      d.fx = d.x;
      d.fy = d.y;
    }

    function dragged(event, d) {
      d.fx = event.x;
      d.fy = event.y;
    }

    function dragended(event, d) {
      simulation.alphaTarget(0);
      d.fx = null;
      d.fy = null;
    }

    function updateMetrics() {
      document.getElementById("uniformity").textContent = computeUniformity(nodes).toFixed(3);
      document.getElementById("stddev").textContent = computeEdgeLengthStd(links).toFixed(2);
      document.getElementById("crossings").textContent = computeEdgeCrossings(links);
      document.getElementById("angular").textContent = computeAngularUniformity(nodes, links).toFixed(2);
      document.getElementById("tightness").textContent = computeClusterTightness(nodes, links).toFixed(2);
      document.getElementById("symmetry").textContent = computeSymmetry(nodes).toFixed(2);
    }

    function computeUniformity(nodes) {
      const gridSize = 4;
      const cellW = width / gridSize;
      const cellH = height / gridSize;
      const grid = Array(gridSize * gridSize).fill(0);
      nodes.forEach(n => {
        const i = Math.floor(n.x / cellW);
        const j = Math.floor(n.y / cellH);
        if (i >= 0 && j >= 0 && i < gridSize && j < gridSize) {
          grid[j * gridSize + i]++;
        }
      });
      const mean = d3.mean(grid);
      const std = Math.sqrt(d3.mean(grid.map(v => (v - mean) ** 2)));
      return 1 - std / (mean + 1e-5); // Normalized
    }

    function computeEdgeLengthStd(links) {
      const lengths = links.map(l => {
        const dx = l.source.x - l.target.x;
        const dy = l.source.y - l.target.y;
        return Math.sqrt(dx * dx + dy * dy);
      });
      const mean = d3.mean(lengths);
      const std = Math.sqrt(d3.mean(lengths.map(l => (l - mean) ** 2)));
      return std;
    }

    function computeEdgeCrossings(links) {
      let crossings = 0;
      for (let i = 0; i < links.length; i++) {
        for (let j = i + 1; j < links.length; j++) {
          const a = links[i], b = links[j];
          if (intersect(a.source, a.target, b.source, b.target)) {
            crossings++;
          }
        }
      }
      return crossings;
    }
    //对称性
    function computeSymmetry(nodes) {
      const cx = d3.mean(nodes, d => d.x);
      const cy = d3.mean(nodes, d => d.y);

      let symmetryScore = 0;

      nodes.forEach(n => {
        const mirror = nodes.find(m => Math.abs(m.x - (2 * cx - n.x)) < 20 && Math.abs(m.y - (2 * cy - n.y)) < 20);
        if (mirror) symmetryScore++;
      });

      return symmetryScore / nodes.length; // 越接近1越对称
    }
    //群体紧密型
    function computeClusterTightness(nodes, links) {
      const neighborMap = new Map();
      nodes.forEach(n => neighborMap.set(n.id, new Set()));
      links.forEach(l => {
        neighborMap.get(l.source.id || l.source).add(l.target.id || l.target);
        neighborMap.get(l.target.id || l.target).add(l.source.id || l.source);
      });

      let totalRatio = 0, count = 0;

      nodes.forEach(n => {
        const neighbors = Array.from(neighborMap.get(n.id));
        if (neighbors.length < 2) return;

        let edgesBetween = 0;
        for (let i = 0; i < neighbors.length; i++) {
          for (let j = i + 1; j < neighbors.length; j++) {
            if (neighborMap.get(neighbors[i]).has(neighbors[j])) {
              edgesBetween++;
            }
          }
        }
        const maxEdges = neighbors.length * (neighbors.length - 1) / 2;
        totalRatio += edgesBetween / maxEdges;
        count++;
      });

      return totalRatio / count;
    }
    //角度分布均匀性
    function computeAngularUniformity(nodes, links) {
      let totalVariance = 0;
      let count = 0;

      const neighbors = new Map();
      nodes.forEach(n => neighbors.set(n.id, []));

      links.forEach(l => {
        neighbors.get(l.source.id || l.source).push(l.target);
        neighbors.get(l.target.id || l.target).push(l.source);
      });

      nodes.forEach(n => {
        const nbs = neighbors.get(n.id);
        if (nbs.length < 2) return;

        const angles = nbs.map(nb => Math.atan2(nb.y - n.y, nb.x - n.x));
        angles.sort((a, b) => a - b);

        let diffs = [];
        for (let i = 0; i < angles.length; i++) {
          const diff = (angles[(i + 1) % angles.length] - angles[i] + 2 * Math.PI) % (2 * Math.PI);
          diffs.push(diff);
        }

        const mean = 2 * Math.PI / diffs.length;
        const variance = diffs.reduce((sum, d) => sum + (d - mean) ** 2, 0) / diffs.length;

        totalVariance += variance;
        count++;
      });

      return 1 - (totalVariance / count); // 越接近1越均匀
    }


    function distance(p1, p2) {
      const dx = p1.x - p2.x;
      const dy = p1.y - p2.y;
      return Math.sqrt(dx * dx + dy * dy);
    }


    function intersect(p1, p2, p3, p4) {
      const det = (p2.x - p1.x) * (p4.y - p3.y) - (p4.x - p3.x) * (p2.y - p1.y);
      if (det === 0) return false;
      const lambda = ((p4.y - p3.y) * (p4.x - p1.x) + (p3.x - p4.x) * (p4.y - p1.y)) / det;
      const gamma = ((p1.y - p2.y) * (p4.x - p1.x) + (p2.x - p1.x) * (p4.y - p1.y)) / det;
      return (0 < lambda && lambda < 1) && (0 < gamma && gamma < 1);
    }

  </script>
</body>

</html>